package com.netx.ebs;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;

import com.netx.generics.basic.Checker;
import com.netx.generics.basic.Context;
import com.netx.generics.basic.Logger;
import com.netx.generics.basic.DefaultLogger;
import com.netx.generics.basic.ErrorList;
import com.netx.generics.basic.ErrorListException;
import com.netx.generics.basic.RuntimeErrorListException;
import com.netx.generics.basic.IntegrityException;
import com.netx.generics.io.FileSystem;
import com.netx.generics.io.FsContext;
import com.netx.generics.io.Location;
import com.netx.generics.util.Tools;
import com.netx.data.DatabaseException;


public abstract class EbsServlet extends HttpServlet {

	private enum METHOD {
		GET,
		POST
	}
	
	private final SecurityChecker[] _checkers;

	// Constructor for EBS Servlets:
	EbsServlet(SecurityChecker[] array) {
		super();
		_checkers = array;
	}
	
	// Default constructor for application Servlets:
	protected EbsServlet() {
		super();
		_checkers = new SecurityChecker[] {
			new ApplicationStateChecker()
		};
	}

	public final void init(ServletConfig config) {
		try {
			super.init(config);
			// Set the application-wide EbsContext, initialized with default values for basic properties.
			// This makes a basic EbsContext always available for EBS Servlets. In the most unrecoverable
			// situation possible (not having called [initializer].init), control is handed to [error-handler]
			// and [error-handler].init is called, therefore setting the EbsContext.
			// At this point, whatever properties that can be initialized are; the rest is done in InitializerServlet.
			if(getEbsContext() == null) {
				final EbsContext ebsCtx = new EbsContext();
				// Just to load the "filesystems" context:
				new FsContext();
				getServletContext().setAttribute(Constants.SRVCTX_EBSCTX, ebsCtx);
				// Load 'application-root' and 'application-name' properties:
				FileSystem appRoot = new FileSystem(new Location(getServletContext().getRealPath("/")));
				ebsCtx.setProperty("application-root", appRoot);
				ebsCtx.setProperty("application-name", appRoot.getName());
			}
			// Call the new init handler. This will catch any init errors
			// thrown by specialized ebsInit implementations, and
			// handle them correctly.
			try {
				// This will cause specialized ebsInit implementations to be called
				// only if the application has loaded correctly, therefore minimizing 
				// the number of errors reported.
				if(this instanceof InitializerServlet || applicationInitialized()) {
					ebsInit(config);
				}
			}
			catch(Throwable t) {
				addInitError(t);
			}
		}
		catch(Throwable t) {
			// This is not supposed to happen, but prepare for it anyway:
			Tools.handleCriticalError(t);
		}
	}

	// This cannot be done, otherwise GenericServlet enters an infinite cycle
	// and ends up by throwing a StackOverflowException
	//public void init() {
	//	init(this);
	//}

	public final void destroy() {
		try {
			super.destroy();
			ebsDestroy();
		}
		catch(Throwable t) {
			getLogger().error(t);
		}
	}

	protected void ebsInit(ServletConfig config) throws Throwable {
		// noop; default (empty) implementation.
	}

	protected void ebsDestroy() throws Throwable {
		// noop; default (empty) implementation.
	}

	protected EbsContext getEbsContext() {
		return (EbsContext)getServletContext().getAttribute(Constants.SRVCTX_EBSCTX);
	}

	protected Logger getLogger() {
		return Context.getRootContext().getLogger();
	}
	
	protected final void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
		try {
			_secureService(request, response, METHOD.GET);
		}
		catch(DatabaseException de) {
			// Wrap exception in a runtime exception:
			throw new DatabaseExceptionWrapper(de);
		}
	}

	protected final void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
		try {
			_secureService(request, response, METHOD.POST);
		}
		catch(DatabaseException de) {
			// Wrap exception in a runtime exception:
			throw new DatabaseExceptionWrapper(de);
		}
	}

	protected void doGet(EbsRequest request, EbsResponse response) throws IOException, DatabaseException {
		// Default implementation: use HttpServlet's implementation
		try {
			super.doGet(request, response);
		}
		catch(ServletException se) {
			throw new IntegrityException(se);
		}
	}

	protected void doPost(EbsRequest request, EbsResponse response) throws IOException, DatabaseException {
		// Default implementation: use HttpServlet's implementation
		try {
			super.doPost(request, response);
		}
		catch(ServletException se) {
			throw new IntegrityException(se);
		}
	}

	//TODO change impl: if args come null, just log a warning
	protected void addInitError(String message) {
		Checker.checkEmpty(message, "message");
		new DefaultLogger().error(message, null);
		_addInitError(new InitError(message));
	}

	protected void addInitError(Throwable t) {
		Checker.checkNull(t, "t");
		if(t instanceof ErrorListException) {
			ErrorList el = ((ErrorListException)t).getContents();
			Tools.log(new DefaultLogger(), el);
			_addInitError(new InitError(el));
		}
		else if(t instanceof RuntimeErrorListException) {
			ErrorList el = ((RuntimeErrorListException)t).getContents();
			Tools.log(new DefaultLogger(), el);
			_addInitError(new InitError(el));
		}
		else {
			new DefaultLogger().error(t);
			_addInitError(new InitError(t));
		}
	}

	protected void addInitError(ErrorList el) {
		Checker.checkNull(el, "el");
		Tools.log(new DefaultLogger(), el);
		_addInitError(new InitError(el));
	}
	
	private void _addInitError(InitError ie) {
		@SuppressWarnings("unchecked")
		Map<String,List<InitError>> errorMap = (Map<String,List<InitError>>)getServletContext().getAttribute(Constants.SRVCTX_ERROR_LIST);
		// Init application error map:
		// This way, addInitError works even if InitializarServlet.init hasn't been called.
		if(errorMap == null) {
			errorMap = new HashMap<String,List<InitError>>();
			getServletContext().setAttribute(Constants.SRVCTX_ERROR_LIST, errorMap);
		}
		// Init error list for this Servlet:
		List<InitError> l = errorMap.get(getServletName());
		if(l == null) {
			l = new ArrayList<InitError>();
			errorMap.put(getServletName(), l);
		}
		l.add(ie);
	}

	// module protected: for self, AuthenticatedServlet, InitializerServlet:
	boolean applicationInitialized() {
		return getServletContext().getAttribute(Constants.SRVCTX_ERROR_LIST) == null;
	}

	private void _secureService(HttpServletRequest request, HttpServletResponse response, METHOD method) throws IOException, DatabaseException {
		ServletContext srvCtx = getServletContext();
		EbsContext ebsCtx = getEbsContext();
		request.setCharacterEncoding(ebsCtx.getCharset());
		EbsRequestImpl  request2  = new EbsRequestImpl(request, srvCtx, ebsCtx);
		EbsResponseImpl response2 = new EbsResponseImpl(response, srvCtx, ebsCtx);
		for(int i=0; i<_checkers.length; i++) {
			_checkers[i].check(request2, response2);
		}
		if(method == METHOD.GET) {
			doGet(request2, response2);
		}
		else if(method == METHOD.POST) {
			doPost(request2, response2);
		}
		else {
			// Impossible, _secureService is called by doGet or doPost
			throw new IntegrityException(method);
		}
		request2.closeConnection();
	}
}